🔐 DRF Authentication

Session- und Token-Authentifizierung

Django REST Framework - Modul 4

Von Session-Auth über Token-Auth bis JWT

📋 Agenda

🔍 Grundlagen

  • Was ist Authentifizierung?
  • Authentication vs Authorization
  • DRF Authentication System

🍪 Session Authentication

  • Wie funktioniert Session-Auth?
  • Setup & Konfiguration
  • Login/Logout Endpoints
  • Praxis-Beispiel

🎫 Token Authentication

  • Token-Auth Konzept
  • DRF Token Setup
  • Token generieren & verwenden
  • Praxis-Beispiel

🔑 JWT Authentication

  • Was ist JWT?
  • SimpleJWT Installation
  • Access & Refresh Tokens
  • Praxis-Beispiel

🎯 Permissions

  • Permission Classes
  • Custom Permissions
  • Object-Level Permissions

💡 Best Practices

  • Sicherheitsrichtlinien
  • Testing Authentication
  • Production Setup

🔍 Was ist Authentifizierung?

Definition

Authentifizierung = "Wer bist du?"

Autorisierung = "Was darfst du?"

🔐 Authentication (Authentifizierung)

  • ✅ Identität des Users prüfen
  • ✅ Login-Prozess
  • ✅ Credentials validieren
  • ✅ User-Objekt bereitstellen

Beispiel:

Username: john_doe
Password: secret123

→ User ist "John Doe"

🎫 Authorization (Autorisierung)

  • ✅ Zugriffsrechte prüfen
  • ✅ Permissions kontrollieren
  • ✅ Ressourcen-Zugriff erlauben/verweigern
  • ✅ Rollen & Berechtigungen

Beispiel:

User: john_doe
Role: Editor

→ Darf Filme erstellen & bearbeiten
→ Darf Filme NICHT löschen

🔄 Ablauf:

1. Authentication: User identifizieren (Login)
   ↓
2. Authorization: Rechte prüfen (Permissions)
   ↓
3. Access: Zugriff erlauben oder verweigern

🎯 DRF Authentication System

Wie funktioniert Authentication in DRF?

1. Request-Lifecycle:

HTTP Request
    ↓
DRF Authentication Classes prüfen Request
    ↓
request.user wird gesetzt (User-Objekt oder AnonymousUser)
    ↓
request.auth wird gesetzt (Token, None, etc.)
    ↓
View verarbeitet Request
    ↓
Permission Classes prüfen request.user
    ↓
Response wird zurückgegeben

2. DRF Authentication Classes (Built-in):

from rest_framework.authentication import (
    SessionAuthentication,     # ← Cookie-basiert (Browser)
    BasicAuthentication,       # ← Username:Password in Header
    TokenAuthentication,       # ← Token in Header
)

🍪 SessionAuthentication

Gut für:

  • Web-Browsable API
  • Same-Origin Requests
  • Django Templates

Cookie-basiert

🎫 TokenAuthentication

Gut für:

  • Mobile Apps
  • SPA (React, Vue)
  • Third-Party Clients

Token in Header

🔑 JWT (SimpleJWT)

Gut für:

  • Stateless Auth
  • Microservices
  • Modern Apps

Empfohlen!

🚀 Projekt Setup

Vorbereitungen - Django Projekt erstellen

1. Django Projekt & App erstellen:

# Neues Projekt erstellen
django-admin startproject movieapi .
python manage.py startapp movies

# Projektstruktur:
movieapi/
├── movieapi/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── movies/
│   ├── __init__.py
│   ├── models.py
│   ├── views.py
│   ├── serializers.py
│   ├── urls.py
│   └── admin.py
└── manage.py

2. Django REST Framework installieren:

# Installation
pip install djangorestframework

# Installierte Version prüfen
pip show djangorestframework

3. settings.py - Apps registrieren:

# filepath: movieapi/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # DRF
    'rest_framework',
    
    # Unsere App
    'movies',
]

📦 Models erstellen

4. Models definieren:

# filepath: movies/models.py
from django.db import models

class Movie(models.Model):
    """Model für Filme"""
    title = models.CharField(max_length=200, verbose_name="Titel")
    year = models.IntegerField(verbose_name="Erscheinungsjahr")
    genre = models.CharField(max_length=100, verbose_name="Genre", blank=True)
    rating = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True, verbose_name="Bewertung")
    description = models.TextField(verbose_name="Beschreibung", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        verbose_name = "Film"
        verbose_name_plural = "Filme"
        ordering = ['-year', 'title']
    
    def __str__(self):
        return f"{self.title} ({self.year})"


class Artist(models.Model):
    """Model für Schauspieler/Künstler"""
    first_name = models.CharField(max_length=100, verbose_name="Vorname")
    last_name = models.CharField(max_length=100, verbose_name="Nachname")
    birth_date = models.DateField(verbose_name="Geburtsdatum", null=True, blank=True)
    nationality = models.CharField(max_length=100, verbose_name="Nationalität", blank=True)
    biography = models.TextField(verbose_name="Biografie", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        verbose_name = "Künstler"
        verbose_name_plural = "Künstler"
        ordering = ['last_name', 'first_name']
    
    def __str__(self):
        return f"{self.first_name} {self.last_name}"
    
    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"


class MovieCasting(models.Model):
    """Brückentabelle: Welcher Artist spielt in welchem Movie"""
    movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='castings', verbose_name="Film")
    artist = models.ForeignKey(Artist, on_delete=models.CASCADE, related_name='movie_roles', verbose_name="Künstler")
    role_name = models.CharField(max_length=200, verbose_name="Rollenname")
    is_main_role = models.BooleanField(default=False, verbose_name="Hauptrolle")
    order = models.IntegerField(default=0, verbose_name="Reihenfolge")
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name = "Besetzung"
        verbose_name_plural = "Besetzungen"
        ordering = ['order', 'artist__last_name']
        unique_together = [['movie', 'artist', 'role_name']]
    
    def __str__(self):
        role_type = "Hauptrolle" if self.is_main_role else "Nebenrolle"
        return f"{self.artist.full_name} als {self.role_name} in {self.movie.title} ({role_type})"

🔧 Migrations & Admin Setup

5. Migrations erstellen & anwenden:

# Migrations erstellen
python manage.py makemigrations

# Output:
Migrations for 'movies':
  movies/migrations/0001_initial.py
    - Create model Movie
    - Create model Artist
    - Create model MovieCasting

# Migrations anwenden
python manage.py migrate

# Superuser erstellen
python manage.py createsuperuser
# Username: admin
# Email: admin@example.com
# Password: admin123

6. Admin registrieren:

# filepath: movies/admin.py
from django.contrib import admin
from .models import Movie, Artist, MovieCasting


@admin.register(Movie)
class MovieAdmin(admin.ModelAdmin):
    list_display = ['title', 'year', 'genre', 'rating']
    list_filter = ['genre', 'year']
    search_fields = ['title', 'description']
    ordering = ['-year', 'title']


@admin.register(Artist)
class ArtistAdmin(admin.ModelAdmin):
    list_display = ['full_name', 'nationality', 'birth_date']
    list_filter = ['nationality']
    search_fields = ['first_name', 'last_name']
    ordering = ['last_name', 'first_name']


@admin.register(MovieCasting)
class MovieCastingAdmin(admin.ModelAdmin):
    list_display = ['artist', 'role_name', 'movie', 'is_main_role', 'order']
    list_filter = ['is_main_role', 'movie']
    search_fields = ['role_name', 'artist__first_name', 'artist__last_name']
    ordering = ['order']

✅ Test:

# Server starten
python manage.py runserver

# Admin öffnen: http://localhost:8000/admin/
# Login: admin / admin123
# Testdaten erstellen!

📝 Serializers erstellen

7. Serializers definieren:

# filepath: movies/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Movie, Artist, MovieCasting


class MovieSerializer(serializers.ModelSerializer):
    """Serializer für Movie CRUD"""
    class Meta:
        model = Movie
        fields = '__all__'
        read_only_fields = ['id', 'created_at', 'updated_at']


class ArtistSerializer(serializers.ModelSerializer):
    """Serializer für Artist CRUD"""
    class Meta:
        model = Artist
        fields = '__all__'
        read_only_fields = ['id', 'created_at', 'updated_at']


class MovieCastingSerializer(serializers.ModelSerializer):
    """Serializer für MovieCasting CRUD"""
    artist_name = serializers.CharField(source='artist.full_name', read_only=True)
    movie_title = serializers.CharField(source='movie.title', read_only=True)
    
    class Meta:
        model = MovieCasting
        fields = '__all__'
        read_only_fields = ['id', 'created_at']


class UserSerializer(serializers.ModelSerializer):
    """Serializer für User-Registrierung"""
    password = serializers.CharField(write_only=True, required=True, style={'input_type': 'password'})
    password2 = serializers.CharField(write_only=True, required=True, style={'input_type': 'password'}, label='Password confirmation')
    
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'password', 'password2', 'first_name', 'last_name']
        read_only_fields = ['id']
    
    def validate(self, attrs):
        """Passwörter müssen übereinstimmen"""
        if attrs['password'] != attrs['password2']:
            raise serializers.ValidationError({"password": "Password fields didn't match."})
        return attrs
    
    def create(self, validated_data):
        """User erstellen mit gehashtem Passwort"""
        validated_data.pop('password2')  # password2 nicht speichern
        
        user = User.objects.create_user(
            username=validated_data['username'],
            email=validated_data.get('email', ''),
            password=validated_data['password'],
            first_name=validated_data.get('first_name', ''),
            last_name=validated_data.get('last_name', '')
        )
        return user


class UserDetailSerializer(serializers.ModelSerializer):
    """Serializer für User-Details (ohne Passwort)"""
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'first_name', 'last_name', 'is_staff', 'date_joined']
        read_only_fields = ['id', 'is_staff', 'date_joined']

🍪 Session Authentication - Theorie

Wie funktioniert Session-basierte Authentifizierung?

Ablauf:

1. User sendet Login (Username + Password)
   ↓
2. Django prüft Credentials
   ↓
3. Django erstellt Session in DB
   ↓
4. Django sendet sessionid Cookie an Browser
   ↓
5. Browser sendet Cookie bei jedem Request
   ↓
6. Django lädt Session aus DB → request.user

✅ Vorteile

  • Einfach zu implementieren
  • Django built-in
  • Gut für Browsable API
  • CSRF-Protection integriert
  • Session-Daten auf Server

❌ Nachteile

  • Stateful (Session in DB)
  • Skalierung schwieriger
  • Nur Same-Origin
  • Nicht für Mobile Apps
  • CORS-Probleme

🎯 Gut für:

  • Web-Browsable API (DRF UI)
  • Django Templates + API
  • Same-Origin Requests
  • Development & Testing

🔧 Session Authentication - Setup

8. DRF Settings konfigurieren:

# filepath: movieapi/settings.py
REST_FRAMEWORK = {
    # Authentication Classes
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
    ],
    
    # Permission Classes (optional)
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
}

9. ViewSets erstellen:

# filepath: movies/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
from .models import Movie, Artist, MovieCasting
from .serializers import MovieSerializer, ArtistSerializer, MovieCastingSerializer


class MovieViewSet(viewsets.ModelViewSet):
    """ViewSet für Movie CRUD"""
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]  # Lesen für alle, Schreiben nur Auth
    
    @action(detail=False, methods=['get'])
    def my_favorites(self, request):
        """Nur für authenticated Users"""
        if not request.user.is_authenticated:
            return Response(
                {'detail': 'Authentication credentials were not provided.'},
                status=status.HTTP_401_UNAUTHORIZED
            )
        
        # Beispiel: Filme nach Rating sortiert
        movies = Movie.objects.filter(rating__gte=8.0).order_by('-rating')[:10]
        serializer = self.get_serializer(movies, many=True)
        return Response(serializer.data)


class ArtistViewSet(viewsets.ModelViewSet):
    """ViewSet für Artist CRUD"""
    queryset = Artist.objects.all()
    serializer_class = ArtistSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]


class MovieCastingViewSet(viewsets.ModelViewSet):
    """ViewSet für MovieCasting CRUD"""
    queryset = MovieCasting.objects.select_related('movie', 'artist').all()
    serializer_class = MovieCastingSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

🔗 Session Authentication - URLs

10. URLs konfigurieren:

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet, MovieCastingViewSet

router = DefaultRouter()
router.register(r'movies', MovieViewSet)
router.register(r'artists', ArtistViewSet)
router.register(r'castings', MovieCastingViewSet)

urlpatterns = router.urls

11. Haupt-URLs:

# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    
    # API
    path('api/', include('movies.urls')),
    
    # DRF Auth (Login/Logout für Browsable API)
    path('api-auth/', include('rest_framework.urls')),
]

✅ Test - Session Authentication:

# Server starten
python manage.py runserver

# Browser öffnen:
http://localhost:8000/api/

# Oben rechts: "Log in" Button
# Login mit Superuser: admin / admin123

# Jetzt kannst du:
# - Filme erstellen (POST /api/movies/)
# - Filme bearbeiten (PUT /api/movies/1/)
# - Filme löschen (DELETE /api/movies/1/)

🧪 Session Authentication - Testing

Test in Browsable API:

# 1. Ohne Login: http://localhost:8000/api/movies/
# → GET funktioniert (Read)
# → POST-Formular nicht sichtbar (nur Auth Users)

# 2. Login: http://localhost:8000/api-auth/login/
# → Login mit admin / admin123

# 3. Nach Login: http://localhost:8000/api/movies/
# → POST-Formular sichtbar!
# → Film erstellen möglich

# 4. Test: Film erstellen
{
  "title": "The Matrix",
  "year": 1999,
  "genre": "Sci-Fi",
  "rating": 8.7,
  "description": "A hacker discovers reality is a simulation."
}

# 5. Custom Action testen:
# http://localhost:8000/api/movies/my_favorites/
# → Funktioniert nur wenn eingeloggt!

Test mit cURL (Session):

# 1. CSRF Token holen
curl -c cookies.txt http://localhost:8000/api/

# 2. Login
curl -b cookies.txt -c cookies.txt \
  -X POST http://localhost:8000/api-auth/login/ \
  -d "username=admin&password=admin123&csrfmiddlewaretoken=CSRF_TOKEN"

# 3. API Request (mit Session Cookie)
curl -b cookies.txt http://localhost:8000/api/movies/

# ← Session Cookie wird automatisch mitgesendet!

Hinweis: Session-Auth ist hauptsächlich für Browsable API gedacht. Für echte Clients (Mobile, SPA) nutze Token-Auth!

🎫 Token Authentication - Theorie

Wie funktioniert Token-basierte Authentifizierung?

Ablauf:

1. User sendet Login (Username + Password)
   ↓
2. Django prüft Credentials
   ↓
3. Django erstellt Token & speichert in DB
   ↓
4. Server sendet Token als JSON
   ↓
5. Client speichert Token (localStorage, Cookie, etc.)
   ↓
6. Client sendet Token in Header bei jedem Request:
   Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
   ↓
7. Django lädt User anhand Token → request.user

✅ Vorteile

  • Stateless (kein Session-Lookup)
  • Gut für Mobile Apps
  • Gut für SPAs (React, Vue)
  • Cross-Origin möglich
  • Einfach zu implementieren

❌ Nachteile

  • Token in DB gespeichert
  • Keine Expiration (standardmäßig)
  • Token kann nicht invalidiert werden
  • Ein Token pro User
  • Nicht so sicher wie JWT

🎯 Gut für:

  • Mobile Apps (iOS, Android)
  • Single Page Applications (React, Vue, Angular)
  • Third-Party API Clients
  • Einfache Token-Auth ohne komplexe Requirements

📦 Token Authentication - Installation

12. DRF Token installieren:

# Installation: Bereits in DRF enthalten!
# Nur aktivieren in settings.py

# filepath: movieapi/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # DRF
    'rest_framework',
    'rest_framework.authtoken',  # ← Token-Auth aktivieren!
    
    # Unsere App
    'movies',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',  # Für Browsable API
        'rest_framework.authentication.TokenAuthentication',    # ← Token-Auth hinzufügen!
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
}

13. Migrations anwenden:

# Token-Tabelle erstellen
python manage.py migrate

# Output:
Running migrations:
  Applying authtoken.0001_initial... OK
  Applying authtoken.0002_auto... OK
  Applying authtoken.0003_tokenproxy... OK

# Token-Tabelle in DB:
# - auth_token (key, user_id, created)

🔐 Token Authentication - Login Endpoint

14. Login/Register Views erstellen:

# filepath: movies/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.permissions import AllowAny, IsAuthenticated
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from .serializers import UserSerializer, UserDetailSerializer


@api_view(['POST'])
@permission_classes([AllowAny])
def register(request):
    """
    User registrieren & Token zurückgeben
    POST /api/auth/register/
    Body: {"username": "john", "email": "john@example.com", "password": "secret", "password2": "secret"}
    """
    serializer = UserSerializer(data=request.data)
    
    if serializer.is_valid():
        user = serializer.save()
        
        # Token erstellen
        token, created = Token.objects.get_or_create(user=user)
        
        return Response({
            'token': token.key,
            'user': UserDetailSerializer(user).data
        }, status=status.HTTP_201_CREATED)
    
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
    """
    User login & Token zurückgeben
    POST /api/auth/login/
    Body: {"username": "john", "password": "secret"}
    """
    username = request.data.get('username')
    password = request.data.get('password')
    
    if not username or not password:
        return Response(
            {'error': 'Please provide both username and password'},
            status=status.HTTP_400_BAD_REQUEST
        )
    
    # User authentifizieren
    user = authenticate(username=username, password=password)
    
    if not user:
        return Response(
            {'error': 'Invalid credentials'},
            status=status.HTTP_401_UNAUTHORIZED
        )
    
    # Token erstellen/holen
    token, created = Token.objects.get_or_create(user=user)
    
    return Response({
        'token': token.key,
        'user': UserDetailSerializer(user).data
    })


@api_view(['POST'])
@permission_classes([IsAuthenticated])
def logout(request):
    """
    User logout - Token löschen
    POST /api/auth/logout/
    Header: Authorization: Token 
    """
    try:
        # Token des aktuellen Users löschen
        request.user.auth_token.delete()
        return Response({'message': 'Successfully logged out'}, status=status.HTTP_200_OK)
    except Exception as e:
        return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


@api_view(['GET'])
@permission_classes([IsAuthenticated])
def user_profile(request):
    """
    Aktueller User
    GET /api/auth/me/
    Header: Authorization: Token 
    """
    serializer = UserDetailSerializer(request.user)
    return Response(serializer.data)

🔗 Token Authentication - URLs

15. Auth URLs hinzufügen:

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from django.urls import path
from .views import (
    MovieViewSet, ArtistViewSet, MovieCastingViewSet,
    register, login, logout, user_profile
)

router = DefaultRouter()
router.register(r'movies', MovieViewSet)
router.register(r'artists', ArtistViewSet)
router.register(r'castings', MovieCastingViewSet)

urlpatterns = [
    # Auth Endpoints
    path('auth/register/', register, name='auth-register'),
    path('auth/login/', login, name='auth-login'),
    path('auth/logout/', logout, name='auth-logout'),
    path('auth/me/', user_profile, name='auth-me'),
] + router.urls

Komplette Datei movies/views.py:

# filepath: movies/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import api_view, action, permission_classes
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.permissions import AllowAny, IsAuthenticated, IsAuthenticatedOrReadOnly
from django.contrib.auth import authenticate
from django.contrib.auth.models import User

from .models import Movie, Artist, MovieCasting
from .serializers import (
    MovieSerializer, ArtistSerializer, MovieCastingSerializer,
    UserSerializer, UserDetailSerializer
)


# ============= Authentication Views =============

@api_view(['POST'])
@permission_classes([AllowAny])
def register(request):
    """User registrieren & Token zurückgeben"""
    serializer = UserSerializer(data=request.data)
    
    if serializer.is_valid():
        user = serializer.save()
        token, created = Token.objects.get_or_create(user=user)
        
        return Response({
            'token': token.key,
            'user': UserDetailSerializer(user).data
        }, status=status.HTTP_201_CREATED)
    
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
    """User login & Token zurückgeben"""
    username = request.data.get('username')
    password = request.data.get('password')
    
    if not username or not password:
        return Response(
            {'error': 'Please provide both username and password'},
            status=status.HTTP_400_BAD_REQUEST
        )
    
    user = authenticate(username=username, password=password)
    
    if not user:
        return Response(
            {'error': 'Invalid credentials'},
            status=status.HTTP_401_UNAUTHORIZED
        )
    
    token, created = Token.objects.get_or_create(user=user)
    
    return Response({
        'token': token.key,
        'user': UserDetailSerializer(user).data
    })


@api_view(['POST'])
@permission_classes([IsAuthenticated])
def logout(request):
    """User logout - Token löschen"""
    try:
        request.user.auth_token.delete()
        return Response({'message': 'Successfully logged out'}, status=status.HTTP_200_OK)
    except Exception as e:
        return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


@api_view(['GET'])
@permission_classes([IsAuthenticated])
def user_profile(request):
    """Aktueller User"""
    serializer = UserDetailSerializer(request.user)
    return Response(serializer.data)


# ============= Resource ViewSets =============

class MovieViewSet(viewsets.ModelViewSet):
    """ViewSet für Movie CRUD"""
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    
    @action(detail=False, methods=['get'])
    def my_favorites(self, request):
        """Nur für authenticated Users"""
        if not request.user.is_authenticated:
            return Response(
                {'detail': 'Authentication credentials were not provided.'},
                status=status.HTTP_401_UNAUTHORIZED
            )
        
        movies = Movie.objects.filter(rating__gte=8.0).order_by('-rating')[:10]
        serializer = self.get_serializer(movies, many=True)
        return Response(serializer.data)


class ArtistViewSet(viewsets.ModelViewSet):
    """ViewSet für Artist CRUD"""
    queryset = Artist.objects.all()
    serializer_class = ArtistSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]


class MovieCastingViewSet(viewsets.ModelViewSet):
    """ViewSet für MovieCasting CRUD"""
    queryset = MovieCasting.objects.select_related('movie', 'artist').all()
    serializer_class = MovieCastingSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

🧪 Token Authentication - Testing

Test 1: User registrieren

# cURL:
curl -X POST http://localhost:8000/api/auth/register/ \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john_doe",
    "email": "john@example.com",
    "password": "secret123",
    "password2": "secret123",
    "first_name": "John",
    "last_name": "Doe"
  }'

# Response:
{
  "token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b",
  "user": {
    "id": 2,
    "username": "john_doe",
    "email": "john@example.com",
    "first_name": "John",
    "last_name": "Doe",
    "is_staff": false,
    "date_joined": "2024-01-15T10:30:00Z"
  }
}

Test 2: User login

# cURL:
curl -X POST http://localhost:8000/api/auth/login/ \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john_doe",
    "password": "secret123"
  }'

# Response:
{
  "token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b",
  "user": {
    "id": 2,
    "username": "john_doe",
    "email": "john@example.com",
    "first_name": "John",
    "last_name": "Doe",
    "is_staff": false,
    "date_joined": "2024-01-15T10:30:00Z"
  }
}

# Token speichern für weitere Requests!

Test 3: User-Profil abrufen (mit Token)

# cURL:
curl -X GET http://localhost:8000/api/auth/me/ \
  -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"

# Response:
{
  "id": 2,
  "username": "john_doe",
  "email": "john@example.com",
  "first_name": "John",
  "last_name": "Doe",
  "is_staff": false,
  "date_joined": "2024-01-15T10:30:00Z"
}

🔐 Token Authentication - API Requests

Test 4: Film erstellen (mit Token)

# cURL:
curl -X POST http://localhost:8000/api/movies/ \
  -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Inception",
    "year": 2010,
    "genre": "Sci-Fi",
    "rating": 8.8,
    "description": "A thief who steals corporate secrets through dream-sharing technology."
  }'

# Response:
{
  "id": 1,
  "title": "Inception",
  "year": 2010,
  "genre": "Sci-Fi",
  "rating": 8.8,
  "description": "A thief who steals corporate secrets through dream-sharing technology.",
  "created_at": "2024-01-15T11:00:00Z",
  "updated_at": "2024-01-15T11:00:00Z"
}

Test 5: Film abrufen (OHNE Token)

# cURL:
curl -X GET http://localhost:8000/api/movies/

# Funktioniert! → IsAuthenticatedOrReadOnly
# Lesen ist für alle erlaubt

# Response:
[
  {
    "id": 1,
    "title": "Inception",
    "year": 2010,
    "genre": "Sci-Fi",
    "rating": 8.8,
    "description": "A thief who steals corporate secrets...",
    "created_at": "2024-01-15T11:00:00Z",
    "updated_at": "2024-01-15T11:00:00Z"
  }
]

Test 6: Film erstellen OHNE Token

# cURL:
curl -X POST http://localhost:8000/api/movies/ \
  -H "Content-Type: application/json" \
  -d '{
    "title": "The Matrix",
    "year": 1999,
    "genre": "Sci-Fi"
  }'

# Response: 401 Unauthorized
{
  "detail": "Authentication credentials were not provided."
}

# ← Schreiben nur für authenticated Users!

Test 7: Logout (Token löschen)

# cURL:
curl -X POST http://localhost:8000/api/auth/logout/ \
  -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"

# Response:
{
  "message": "Successfully logged out"
}

# Token wurde aus DB gelöscht!
# Weitere Requests mit diesem Token schlagen fehl.

🔑 JWT Authentication - Theorie

Was ist JWT (JSON Web Token)?

Stateless, selbst-enthaltene Authentifizierung

JWT Struktur:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG4ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header.Payload.Signature

# Header (Base64):
{
  "alg": "HS256",
  "typ": "JWT"
}

# Payload (Base64):
{
  "user_id": 1,
  "username": "john",
  "exp": 1705329000,  # Expiration
  "iat": 1705325400   # Issued At
}

# Signature (HMAC):
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  SECRET_KEY
)

✅ Vorteile

  • Stateless (kein DB-Lookup)
  • Skalierbar
  • Token-Expiration eingebaut
  • Refresh Tokens
  • Microservices-freundlich
  • Self-contained (User-Info im Token)

❌ Nachteile

  • Token kann nicht invalidiert werden
  • Token größer als einfacher Token
  • Komplexer als Token-Auth
  • Secret Key muss sicher sein

🎯 JWT Flow:

1. Login → Access Token (5-15 min) + Refresh Token (1-7 Tage)
2. API Request mit Access Token
3. Access Token abgelaufen → Refresh Token verwenden
4. Neuer Access Token erhalten
5. Weiter mit neuem Access Token

📦 JWT Authentication - Installation

16. djangorestframework-simplejwt installieren:

# Installation
pip install djangorestframework-simplejwt

# Installierte Version prüfen
pip show djangorestframework-simplejwt

17. Settings konfigurieren:

# filepath: movieapi/settings.py
from datetime import timedelta

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # DRF
    'rest_framework',
    'rest_framework.authtoken',  # Token-Auth (optional, für backward compatibility)
    
    # Unsere App
    'movies',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',      # Browsable API
        'rest_framework.authentication.TokenAuthentication',        # Token-Auth
        'rest_framework_simplejwt.authentication.JWTAuthentication', # ← JWT!
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
}

# JWT Settings
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),      # Access Token gültig für 15 Minuten
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),         # Refresh Token gültig für 7 Tage
    'ROTATE_REFRESH_TOKENS': True,                       # Neuer Refresh Token bei Refresh
    'BLACKLIST_AFTER_ROTATION': True,                    # Alte Refresh Tokens blacklisten
    'UPDATE_LAST_LOGIN': True,                           # last_login aktualisieren
    
    'ALGORITHM': 'HS256',                                # Verschlüsselungs-Algorithmus
    'SIGNING_KEY': SECRET_KEY,                           # Secret Key aus settings
    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,
    
    'AUTH_HEADER_TYPES': ('Bearer',),                    # Authorization: Bearer 
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    
    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
}

🔗 JWT Authentication - URLs

18. JWT URLs hinzufügen:

# filepath: movies/urls.py
from rest_framework.routers import DefaultRouter
from django.urls import path
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
    TokenVerifyView,
)
from .views import (
    MovieViewSet, ArtistViewSet, MovieCastingViewSet,
    register, login, logout, user_profile
)

router = DefaultRouter()
router.register(r'movies', MovieViewSet)
router.register(r'artists', ArtistViewSet)
router.register(r'castings', MovieCastingViewSet)

urlpatterns = [
    # Token-Auth Endpoints
    path('auth/register/', register, name='auth-register'),
    path('auth/login/', login, name='auth-login'),
    path('auth/logout/', logout, name='auth-logout'),
    path('auth/me/', user_profile, name='auth-me'),
    
    # JWT Endpoints
    path('auth/jwt/create/', TokenObtainPairView.as_view(), name='jwt-create'),
    path('auth/jwt/refresh/', TokenRefreshView.as_view(), name='jwt-refresh'),
    path('auth/jwt/verify/', TokenVerifyView.as_view(), name='jwt-verify'),
] + router.urls

📍 JWT Endpoints:

  • POST /api/auth/jwt/create/ → Login (Access + Refresh Token)
  • POST /api/auth/jwt/refresh/ → Refresh Token verwenden → Neuer Access Token
  • POST /api/auth/jwt/verify/ → Token validieren

🧪 JWT Authentication - Testing

Test 1: JWT Login (Access + Refresh Token)

# cURL:
curl -X POST http://localhost:8000/api/auth/jwt/create/ \
  -H "Content-Type: application/json" \
  -d '{
    "username": "admin",
    "password": "admin123"
  }'

# Response:
{
  "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcwNTkzMDgwMCwidXNlcl9pZCI6MX0.xyz...",
  "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA1MzI2NzAwLCJ1c2VyX2lkIjoxfQ.abc..."
}

# Speichern:
# - access → Für API Requests (15 Minuten gültig)
# - refresh → Für neuen Access Token (7 Tage gültig)

Test 2: API Request mit Access Token

# cURL:
curl -X POST http://localhost:8000/api/movies/ \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Interstellar",
    "year": 2014,
    "genre": "Sci-Fi",
    "rating": 8.6,
    "description": "A team of explorers travel through a wormhole in space."
  }'

# Response:
{
  "id": 2,
  "title": "Interstellar",
  "year": 2014,
  "genre": "Sci-Fi",
  "rating": 8.6,
  "description": "A team of explorers travel through a wormhole in space.",
  "created_at": "2024-01-15T12:00:00Z",
  "updated_at": "2024-01-15T12:00:00Z"
}

# ✅ Funktioniert mit JWT Access Token!

🔄 JWT Authentication - Refresh Token

Test 3: Access Token erneuern (Refresh)

# Nach 15 Minuten ist Access Token abgelaufen:
curl -X GET http://localhost:8000/api/movies/1/ \
  -H "Authorization: Bearer "

# Response: 401 Unauthorized
{
  "detail": "Given token not valid for any token type",
  "code": "token_not_valid",
  "messages": [
    {
      "token_class": "AccessToken",
      "token_type": "access",
      "message": "Token is expired"
    }
  ]
}

# Lösung: Refresh Token verwenden
curl -X POST http://localhost:8000/api/auth/jwt/refresh/ \
  -H "Content-Type: application/json" \
  -d '{
    "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcwNTkzMDgwMCwidXNlcl9pZCI6MX0.xyz..."
  }'

# Response: Neuer Access Token!
{
  "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA1MzI3NjAwLCJ1c2VyX2lkIjoxfQ.new..."
}

# Jetzt mit neuem Access Token weiterarbeiten!

Test 4: Token verifizieren

# Token prüfen ob gültig:
curl -X POST http://localhost:8000/api/auth/jwt/verify/ \
  -H "Content-Type: application/json" \
  -d '{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  }'

# Response: 200 OK (Token ist gültig)
{}

# Oder: 401 Unauthorized (Token ungültig/abgelaufen)
{
  "detail": "Token is invalid or expired",
  "code": "token_not_valid"
}

🔄 Workflow:

1. Login → Access Token (15 min) + Refresh Token (7 Tage)
2. API Requests mit Access Token
3. Access Token abgelaufen? → Refresh verwenden
4. Neuer Access Token → Weiter arbeiten
5. Refresh Token abgelaufen? → Neu einloggen

🎯 Permissions - Built-in Classes

Authorization - "Was darfst du?"

DRF Permission Classes:

from rest_framework.permissions import (
    AllowAny,                      # Alle dürfen alles
    IsAuthenticated,               # Nur authenticated Users
    IsAuthenticatedOrReadOnly,     # Lesen für alle, Schreiben nur Auth
    IsAdminUser,                   # Nur Admin (is_staff=True)
    DjangoModelPermissions,        # Django Model Permissions
    DjangoObjectPermissions,       # Object-Level Permissions
)

AllowAny

class MovieViewSet(viewsets.ModelViewSet):
    permission_classes = [AllowAny]
    
# Alle dürfen:
# - GET, POST, PUT, PATCH, DELETE
# Auch ohne Login!

IsAuthenticated

class MovieViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated]
    
# Nur authenticated Users dürfen:
# - GET, POST, PUT, PATCH, DELETE
# Ohne Login: 401 Unauthorized

IsAuthenticatedOrReadOnly

class MovieViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticatedOrReadOnly]
    
# Alle dürfen:
# - GET (Read)
# Nur authenticated Users:
# - POST, PUT, PATCH, DELETE (Write)

IsAdminUser

class MovieViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAdminUser]
    
# Nur Admin (is_staff=True):
# - GET, POST, PUT, PATCH, DELETE
# Normale User: 403 Forbidden

🔐 Custom Permissions

Eigene Permission Classes erstellen

Beispiel 1: IsOwnerOrReadOnly

# filepath: movies/permissions.py
from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom Permission: Nur Owner darf bearbeiten/löschen
    Alle anderen dürfen nur lesen
    """
    
    def has_object_permission(self, request, view, obj):
        # Read permissions (GET, HEAD, OPTIONS) für alle
        if request.method in permissions.SAFE_METHODS:
            return True
        
        # Write permissions nur für Owner
        # Annahme: Model hat 'owner' ForeignKey zu User
        return obj.owner == request.user

Verwendung:

# filepath: movies/models.py
from django.db import models
from django.contrib.auth.models import User

class Movie(models.Model):
    title = models.CharField(max_length=200)
    year = models.IntegerField()
    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='movies')  # ← Owner
    # ... weitere Felder


# filepath: movies/views.py
from rest_framework import viewsets
from .models import Movie
from .serializers import MovieSerializer
from .permissions import IsOwnerOrReadOnly


class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    permission_classes = [IsOwnerOrReadOnly]
    
    def perform_create(self, serializer):
        # Owner automatisch setzen bei Erstellung
        serializer.save(owner=self.request.user)

📝 Ergebnis:

  • ✅ Alle können Filme lesen (GET)
  • ✅ Authenticated Users können Filme erstellen (POST)
  • ✅ Nur Owner kann eigene Filme bearbeiten (PUT/PATCH)
  • ✅ Nur Owner kann eigene Filme löschen (DELETE)

🔐 Custom Permissions - Weitere Beispiele

Beispiel 2: IsAdminOrReadOnly

# filepath: movies/permissions.py
from rest_framework import permissions


class IsAdminOrReadOnly(permissions.BasePermission):
    """
    Admin darf alles, andere nur lesen
    """
    
    def has_permission(self, request, view):
        # Read permissions für alle
        if request.method in permissions.SAFE_METHODS:
            return True
        
        # Write permissions nur für Admin
        return request.user and request.user.is_staff

Beispiel 3: IsPremiumUser

# filepath: movies/permissions.py
from rest_framework import permissions


class IsPremiumUser(permissions.BasePermission):
    """
    Nur Premium-User (Custom User Model mit is_premium)
    """
    message = "You must be a premium user to access this resource."
    
    def has_permission(self, request, view):
        # User muss authenticated UND premium sein
        return (
            request.user and
            request.user.is_authenticated and
            hasattr(request.user, 'is_premium') and
            request.user.is_premium
        )

Beispiel 4: Kombination mehrerer Permissions

# filepath: movies/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .permissions import IsOwnerOrReadOnly, IsAdminOrReadOnly


class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer
    
    def get_permissions(self):
        """
        Unterschiedliche Permissions für unterschiedliche Actions
        """
        if self.action == 'list' or self.action == 'retrieve':
            # Lesen: Alle
            permission_classes = [AllowAny]
        elif self.action == 'create':
            # Erstellen: Nur authenticated
            permission_classes = [IsAuthenticated]
        else:
            # Update/Delete: Nur Owner oder Admin
            permission_classes = [IsAuthenticated, IsOwnerOrReadOnly | IsAdminUser]
        
        return [permission() for permission in permission_classes]

🔒 Best Practices - Security

✅ 1. HTTPS verwenden

# settings.py (Production)
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# Tokens IMMER über HTTPS senden!

✅ 2. SECRET_KEY schützen

# settings.py
import os
from decouple import config  # pip install python-decouple

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)

# .env Datei:
SECRET_KEY=your-super-secret-key-here
DEBUG=False

# .env NICHT in Git committen!
# .gitignore:
.env

✅ 3. Token-Expiration setzen

# JWT: Bereits eingebaut
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}

# DRF Token: Keine Expiration!
# → JWT verwenden für Production!

✅ 4. CORS richtig konfigurieren

# Installation
pip install django-cors-headers

# settings.py
INSTALLED_APPS = [
    'corsheaders',
    # ...
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ...
]

# Production: Nur erlaubte Origins
CORS_ALLOWED_ORIGINS = [
    "https://example.com",
    "https://www.example.com",
]

# Development: Alle erlauben (NUR für Dev!)
CORS_ALLOW_ALL_ORIGINS = True  # ← Nur Dev!

✅ 5. Rate Limiting

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',    # Anonymous: 100 Requests/Tag
        'user': '1000/day'    # Authenticated: 1000 Requests/Tag
    }
}

✅ 6. Sensible Daten nicht exposen

# Serializer: Passwörter nie zurückgeben!
class UserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)  # ← write_only!
    
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'password']
        read_only_fields = ['id']

# Response enthält KEIN password!

🧪 Testing Authentication

Unit Tests für Authentication:

# filepath: movies/tests/test_auth.py
from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework import status
from rest_framework.authtoken.models import Token


class AuthenticationTestCase(TestCase):
    """Tests für Authentication"""
    
    def setUp(self):
        """Test-User erstellen"""
        self.client = APIClient()
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123'
        )
        self.token = Token.objects.create(user=self.user)
    
    def test_register_user(self):
        """Test: User registrieren"""
        data = {
            'username': 'newuser',
            'email': 'new@example.com',
            'password': 'newpass123',
            'password2': 'newpass123'
        }
        
        response = self.client.post('/api/auth/register/', data, format='json')
        
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertIn('token', response.data)
        self.assertIn('user', response.data)
        self.assertEqual(response.data['user']['username'], 'newuser')
    
    def test_login_user(self):
        """Test: User login"""
        data = {
            'username': 'testuser',
            'password': 'testpass123'
        }
        
        response = self.client.post('/api/auth/login/', data, format='json')
        
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertIn('token', response.data)
        self.assertIn('user', response.data)
    
    def test_login_invalid_credentials(self):
        """Test: Login mit falschen Credentials"""
        data = {
            'username': 'testuser',
            'password': 'wrongpass'
        }
        
        response = self.client.post('/api/auth/login/', data, format='json')
        
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
    
    def test_authenticated_request(self):
        """Test: Request mit Token"""
        # Token im Header setzen
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
        
        response = self.client.get('/api/auth/me/')
        
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data['username'], 'testuser')
    
    def test_unauthenticated_request(self):
        """Test: Request ohne Token"""
        response = self.client.post('/api/movies/', {'title': 'Test'}, format='json')
        
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
    
    def test_logout(self):
        """Test: Logout (Token löschen)"""
        self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
        
        response = self.client.post('/api/auth/logout/')
        
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        
        # Token sollte gelöscht sein
        self.assertFalse(Token.objects.filter(user=self.user).exists())


# Tests ausführen:
python manage.py test movies.tests.test_auth

🚀 Production Setup

Production settings.py:

# filepath: movieapi/settings.py
import os
from datetime import timedelta
from decouple import config

# Security
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='').split(',')

# HTTPS
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'

# DRF
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day'
    },
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
}

# JWT
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'UPDATE_LAST_LOGIN': True,
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'AUTH_HEADER_TYPES': ('Bearer',),
}

# CORS
CORS_ALLOWED_ORIGINS = config('CORS_ALLOWED_ORIGINS', default='').split(',')

# Database (PostgreSQL empfohlen)
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': config('DB_NAME'),
        'USER': config('DB_USER'),
        'PASSWORD': config('DB_PASSWORD'),
        'HOST': config('DB_HOST', default='localhost'),
        'PORT': config('DB_PORT', default='5432'),
    }
}

.env Datei (Production):

# .env
SECRET_KEY=your-super-secret-production-key-here
DEBUG=False
ALLOWED_HOSTS=example.com,www.example.com
CORS_ALLOWED_ORIGINS=https://example.com,https://www.example.com

DB_NAME=movieapi_db
DB_USER=movieapi_user
DB_PASSWORD=secure_password
DB_HOST=localhost
DB_PORT=5432

🎯 Zusammenfassung

Was haben wir gelernt?

🔍 Grundlagen

  • ✅ Authentication vs Authorization
  • ✅ DRF Authentication System
  • ✅ Request Lifecycle

🍪 Session Auth

  • ✅ Cookie-basiert
  • ✅ Stateful (DB Session)
  • ✅ Gut für Browsable API
  • ✅ CSRF-Protection

🎫 Token Auth

  • ✅ Header-basiert
  • ✅ Stateless (Token in DB)
  • ✅ Gut für Mobile/SPA
  • ✅ Login/Logout Endpoints

🔑 JWT Auth

  • ✅ Stateless (kein DB-Lookup)
  • ✅ Access + Refresh Tokens
  • ✅ Token Expiration
  • ✅ Production-Ready
  • ⭐ Empfohlen!

🎯 Permissions

  • ✅ Built-in Permission Classes
  • ✅ Custom Permissions
  • ✅ Object-Level Permissions
  • ✅ Kombination von Permissions

💡 Best Practices

  • ✅ HTTPS verwenden
  • ✅ SECRET_KEY schützen
  • ✅ Token Expiration
  • ✅ CORS konfigurieren
  • ✅ Rate Limiting
  • ✅ Testing

📊 Vergleich - Alle Auth-Methoden

🍪 Session Auth

Verwendung:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
    ],
}

✅ Vorteile:

  • Django built-in
  • CSRF-Protection
  • Einfach für Browsable API

❌ Nachteile:

  • Stateful (DB Session)
  • Nur Same-Origin
  • Nicht für Mobile

🎯 Gut für:

  • Browsable API (Dev)
  • Django Templates + API

🎫 Token Auth

Verwendung:

INSTALLED_APPS = [
    'rest_framework.authtoken',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}

✅ Vorteile:

  • Einfach zu implementieren
  • Gut für Mobile/SPA
  • Cross-Origin möglich

❌ Nachteile:

  • Token in DB
  • Keine Expiration
  • Ein Token pro User

🎯 Gut für:

  • Einfache APIs
  • Prototyping

🔑 JWT Auth

Verwendung:

pip install djangorestframework-simplejwt

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}

✅ Vorteile:

  • Stateless (kein DB-Lookup)
  • Token Expiration
  • Refresh Tokens
  • Skalierbar
  • Microservices-freundlich

❌ Nachteile:

  • Komplexer
  • Token größer

🎯 Gut für:

  • Production Apps
  • Mobile Apps
  • Modern SPAs

⭐ Empfohlen für Production!

🎉 Gratulation!

Du beherrschst jetzt DRF Authentication!

✅ Was du jetzt kannst:

  • 🔐 Session Authentication implementieren
  • 🎫 Token Authentication nutzen
  • 🔑 JWT Authentication mit Access & Refresh Tokens
  • 🎯 Permission Classes verwenden
  • 🔒 Custom Permissions erstellen
  • 🧪 Authentication testen
  • 🚀 Production-ready Setup
  • 💡 Best Practices anwenden

🎯 Best Practices gelernt:

  • ✅ JWT für Production verwenden
  • ✅ HTTPS immer aktivieren
  • ✅ SECRET_KEY schützen
  • ✅ Token Expiration setzen
  • ✅ CORS richtig konfigurieren
  • ✅ Rate Limiting aktivieren
  • ✅ Sensible Daten schützen
  • ✅ Tests schreiben

🚀 Nächster Schritt:

Implementiere sichere Authentication in deiner API!

  • Nutze JWT für moderne Apps
  • Implementiere Custom Permissions
  • Schreibe Tests für Authentication
  • Deploy mit Production Settings

Viel Erfolg mit deinen sicheren APIs! 🔐

Keep coding, keep learning! 💻

1 / 32